/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.model;

import java.util.Set;
import java.util.Date;
import java.util.Locale;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Collection;
import java.util.Collections;
import javax.persistence.Lob;
import javax.persistence.Column;
import javax.persistence.Temporal;
import javax.persistence.JoinTable;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.ManyToMany;
import javax.persistence.JoinColumn;
import javax.persistence.CascadeType;
import javax.persistence.TemporalType;
import javax.persistence.MappedSuperclass;
import org.hibernate.validator.Max;
import org.hibernate.validator.Min;
import org.hibernate.validator.NotNull;
import org.hibernate.validator.NotEmpty;
import org.directwebremoting.util.LocalUtil;
import org.directwebremoting.annotations.RemoteProperty;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.model.Document;
import org.internna.iwebmvc.model.MultiDocument;
import org.internna.iwebmvc.utils.CollectionUtils;
import org.internna.iwebmvc.metadata.ui.GridColumn;
import org.internna.iwebjtracker.issue.model.Issue;
import org.internna.iwebmvc.metadata.ui.EntityFilter;
import org.internna.iwebmvc.model.AbstractOwnedDomainEntity;

import static org.internna.iwebmvc.utils.StringUtils.hasText;

/**
 * Contains all the data that an issue needs to persist.
 *
 * @author Jose Noheda
 * @since 1.0
 */
@MappedSuperclass
public abstract class IssueDataHolder extends AbstractOwnedDomainEntity implements Issue<DefaultModule, DefaultVersion, TrackerUser, DefaultProject, IssueComment, DefaultIssue> {

    private static final long serialVersionUID = -1122591727468342313L;

    @Min(1)
    @Max(9999)
    @RemoteProperty
    @Column(name = "ISSUE_ALIAS")
    @GridColumn(relativeWidth = 1.15)
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.NUMBER)
    private int alias = -1;

    @NotNull
    @ManyToOne
    @RemoteProperty
    @JoinColumn(name = "PROJECT")
    private DefaultProject parentProject;

    @NotNull
    @ManyToOne
    @RemoteProperty
    @GridColumn(order = 2)
    @JoinColumn(name = "MODULE")
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENTITY)
    private DefaultModule parentModule;

    @NotNull
    @ManyToOne
    @RemoteProperty
    @JoinColumn(name = "AFFECTED_VERSION")
    private DefaultVersion affectedVersion;

    @ManyToOne
    @RemoteProperty
    @JoinColumn(name = "TARGET_VERSION")
    @GridColumn(order = 4, relativeWidth = 0.5)
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENTITY)
    private DefaultVersion targetVersion;

    @ManyToOne
    @JoinColumn(name = "FIX_VERSION")
    private DefaultVersion fixVersion;

    @NotNull
    @RemoteProperty
    @GridColumn(order = 5, relativeWidth = 0.75)
    @Column(name = "ISSUE_TYPE")
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENUM)
    private Type type;

    @NotNull
    @RemoteProperty
    @Column(name = "STATUS")
    @GridColumn(order = 7, relativeWidth = 0.7)
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENUM)
    private Status status;

    @NotNull
    @RemoteProperty
    @Column(name = "RESOLUTION")
    @GridColumn(order = 8, relativeWidth = 0.7)
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENUM)
    private ResolutionStatus resolutionStatus;

    @NotNull
    @RemoteProperty
    @Column(name = "SEVERITY")
    @GridColumn(order = 6, relativeWidth = 0.7)
    @EntityFilter(type = EntityFilter.WIDGET_TYPE.ENUM)
    private Severity severity;

    @NotNull
    @RemoteProperty
    @Column(name = "CREATION")
    @Temporal(TemporalType.TIMESTAMP)
    private Date creationDate;

    @NotEmpty
    @RemoteProperty
    @Column(name = "SUMMARY")
    @GridColumn(relativeWidth = 2.0, order = 4)
    private String summary;

    @Lob
    @RemoteProperty
    @Column(name = "DESCRIPTION")
    private String description;

    @NotNull
    @ManyToOne
    @JoinColumn(name = "CREATOR", updatable = false)
    private TrackerUser creator;

    @ManyToOne
    @JoinColumn(name = "ASSIGNEE")
    private TrackerUser assignee;

    @Column(name = "TAGS")
    private String tags;

    @ManyToMany
    @JoinTable(joinColumns = {
        @JoinColumn(name = "ISSUE", referencedColumnName = "ID", nullable = false)
    }, inverseJoinColumns = {
        @JoinColumn(name = "VOTER", referencedColumnName = "USERNAME", nullable = false)
    })
    private Set<TrackerUser> voters;

    @ManyToMany
    @JoinTable(joinColumns = {
        @JoinColumn(name = "ISSUE", referencedColumnName = "ID", nullable = false)
    }, inverseJoinColumns = {
        @JoinColumn(name = "WATCHER", referencedColumnName = "USERNAME", nullable = false)
    })
    private Set<TrackerUser> watchers;

    @NotNull
    @RemoteProperty
    @Column(name = "LASTMODIFIED")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModified;

    @OneToMany(mappedBy = "issue", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<IssueComment> comments;

    @JoinColumn(name = "ATTACHMENTS")
    @ManyToOne(cascade = CascadeType.ALL)
    private MultiDocument attachments;

    @OneToMany(mappedBy = "issue", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<Interlink> links;

    /**
     * Accessor method.
     */
    @Override public TrackerUser getCreator() {
        return creator;
    }

    /**
     * Accessor method. Automatically votes for this issue
     */
    @Override public void setCreator(TrackerUser creator) {
        this.creator = creator;
        vote(creator);
        watch(creator);
        if (creator != null) {
            addOwner(creator.getSecurityUser());
        }
    }

    /**
     * Accessor method.
     */
    @Override public DefaultProject getParentProject() {
        return parentProject;
    }

    /**
     * Establish the project of this issue. This has several side effects on the
     * module, affected version and fix version properties that must always match
     * this value.
     *
     * @param project any
     */
    @Override public void setParentProject(DefaultProject project) {
        this.parentProject = project;
        if (project == null) {
            setFixVersion(null);
            setParentModule(null);
            setAffectedVersion(null);
        }
    }

    /**
     * Accessor method.
     */
    @Override public DefaultModule getParentModule() {
        return parentModule;
    }

    /**
     * Establish the module if the project is in accordance.
     *
     * @param module any
     */
    @Override public void setParentModule(DefaultModule module) {
        if (module == null) {
            this.parentModule = null;
        } else {
            DefaultProject project = module.getParentProject();
            if (project != null) {
                if (parentProject == null) {
                    this.parentModule = module;
                    setParentProject(project);
                } else if (this.parentProject.equals(module.getParentProject())) {
                    this.parentModule = module;
                }
            }
        }
    }

    /**
     * Accessor method.
     */
    @Override public String getSummary() {
        return summary;
    }

    /**
     * Accessor method.
     */
    @Override public void setSummary(String summary) {
        this.summary = summary;
    }

    /**
     * Accessor method.
     */
    @Override public String getDescription() {
        return description;
    }

    /**
     * Accessor method.
     */
    @Override public void setDescription(String description) {
        this.description = description;
    }

    /**
     * Accessor method.
     */
    @Override public DefaultVersion getAffectedVersion() {
        return affectedVersion;
    }

    /**
     * Establish the affected version if the project is in accordance.
     *
     * @param module any
     */
    @Override public void setAffectedVersion(DefaultVersion version) {
        if (version == null) {
            this.affectedVersion = null;
        } else {
            DefaultProject project = version.getParentProject();
            if (project != null) {
                if (parentProject == null) {
                    this.affectedVersion = version;
                    setParentProject(project);
                } else if (this.parentProject.equals(version.getParentProject())) {
                    this.affectedVersion = version;
                }
            }
        }
    }

    /**
     * Accessor method.
     *
     * @return any
     */
    public DefaultVersion getTargetVersion() {
        return targetVersion;
    }

    /**
     * The version where this issue would be resolved.
     *
     * @param targetVersion null or any open version
     */
    @Override public void setTargetVersion(DefaultVersion targetVersion) {
        if ((targetVersion == null) || ((targetVersion.isOpen()) && ((parentProject == null) || (parentProject.equals(targetVersion.getParentProject()))))) {
            this.targetVersion = targetVersion;
            if ((parentProject == null) && (targetVersion != null)) {
                parentProject = targetVersion.getParentProject();
            }
        }
    }

    /**
     * Accessor method.
     */
    @Override public DefaultVersion getFixVersion() {
        return fixVersion;
    }

    /**
     * Establish the fix version if the project is in accordance The issue can only have a target version
     * or a fix version active at the same time.
     *
     * @param module any
     */
    @Override public void setFixVersion(DefaultVersion version) {
        if (version == null) {
            this.fixVersion = null;
        } else {
            DefaultProject project = version.getParentProject();
            if (project != null) {
                if (parentProject == null) {
                    this.fixVersion = version;
                    setParentProject(project);
                    if (targetVersion == null) {
                        setTargetVersion(version);
                    }
                } else if (this.parentProject.equals(version.getParentProject())) {
                    this.fixVersion = version;
                    if (targetVersion == null) {
                        setTargetVersion(version);
                    }
                }
            }
        }
    }

    /**
     * Generates an unmodifiable collection from a comma-separated sentence.
     *
     * @return a non null unodifiable collection
     */
    @Override public Collection<String> getTags() {
        return hasText(tags) ? Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(tags.split(",")))) : Collections.EMPTY_SET;
    }

    /**
     * Removes unnecessary spaces and null elements before setting the tag cloud.
     *
     * @param tags (any)
     */
    public void setTags(String tags) {
        this.tags = hasText(tags) ? LocalUtil.join(sanitize(tags.split(",")), ",") : null;
        if (hasText(this.tags)) {
            this.tags = this.tags.replaceAll(",{2,}", ",");
        }
    }

    private String[] sanitize(String[] tags) {
        String[] sane = new String[tags.length];
        for (int index =  0; index < tags.length; index++) {
            sane[index] = hasText(tags[index]) ? tags[index].trim() : "";
        }
        return sane;
    }

    /**
     * Creates a comma-separated string from a collection.
     *
     * @param tags any
     */
    @Override public void setTags(Collection<String> tags) {
        setTags(CollectionUtils.isEmpty(tags) ? null : LocalUtil.join(tags.toArray(), ","));
    }

    /**
     * Accessor method.
     */
    @Override public Date getCreationDate() {
        return creationDate == null ? null : new Date(creationDate.getTime());
    }

    /**
     * Accessor method.
     *
     * @param creationDate a non null date.
     */
    protected void setCreationDate(Date creationDate) {
        Assert.notNull(creationDate);
        this.creationDate = creationDate;
        setLastModified(creationDate);
    }

    /**
     * Accessor method.
     */
    @Override public Severity getSeverity() {
        return severity == null ? Severity.FIX : severity;
    }

    /**
     * Accessor method.
     */
    @Override public void setSeverity(Severity severity) {
        this.severity = severity;
    }

    /**
     * Accessor method.
     */
    @Override public Type getType() {
        return type == null ? Type.BUG : type;
    }

    /**
     * Accessor method.
     */
    @Override public void setType(Type type) {
        this.type = type;
    }

    /**
     * Accessor method.
     */
    @Override public Status getStatus() {
        return status == null ? Status.OPEN : status;
    }

    /**
     * Accessor method.
     */
    @Override public void setStatus(Status status) {
        this.status = status;
    }

    /**
     * Accessor method.
     */
    @Override public ResolutionStatus getResolutionStatus() {
        return resolutionStatus == null ? ResolutionStatus.PENDING : resolutionStatus;
    }

    /**
     * Accessor method.
     */
    @Override public void setResolutionStatus(ResolutionStatus resolutionStatus) {
        this.resolutionStatus = resolutionStatus;
    }

    /**
     * Changes the current alias as long as the value is valid (> 0) and the current
     * value is not (< 0).
     *
     * @param alias any
     */
    public void setAlias(int alias) {
        if ((alias > 0) && (this.alias < 0)) {
            this.alias = alias;
        }
    }

    /**
     * Builds an alias string with the assigned sequence number and the project alias.
     *
     * @return null unless a parent project with a valid alias has been configured
     */
    @Override public String getAlias() {
        return alias > 0 && getParentProject() != null ? getParentProject().getAlias() + "-" + pad(alias) : null;
    }

    private String pad(int number) {
        String zeros = number < 10 ? "000" : number < 100 ? "00" : number < 1000 ? "0" : "";
        return zeros + number;
    }

    /**
     * Accessor method
     */
    @Override public String getResolutionComment() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * Accessor method
     */
    @Override public void setResolutionComment(String comment) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    /**
     * The developer in charge of resolving this issue.
     */
    @Override public TrackerUser getAssignee() {
        return assignee;
    }

    /**
     * Accepts only null or developers of the project
     */
    @Override public void setAssignee(TrackerUser assignee) {
        if ((assignee == null) || ((getParentProject() != null) && (getParentProject().isDeveloper(assignee)))) {
            this.assignee = assignee;
            onAssign(assignee);
        }
    }

    private void onAssign(TrackerUser assignee) {
        Status current = getStatus();
        if ((assignee == null) && (!Status.CLOSED.equals(current))) {
            setStatus(Status.OPEN);
        } else if ((current == null) || (Status.OPEN.equals(current))) {
            setStatus(Status.ASSIGNED);
        }
    }

    /**
     * Group of users that have voted for this issue resolution.
     *
     * @return a non modifiable collection
     */
    public Set<TrackerUser> getVoters() {
        return CollectionUtils.isEmpty(voters) ? Collections.EMPTY_SET : Collections.unmodifiableSet(voters);
    }

    /**
     * Accessor method
     */
    protected void setVoters(Set<TrackerUser> voters) {
        if (CollectionUtils.isNotEmpty(this.voters)) {
            this.voters.clear();
        }
        this.voters = voters;
    }

    /**
     * Group of users that want to be notified of issue changes.
     *
     * @return a non null unmodifiable collection
     */
    public Set<TrackerUser> getWatchers() {
        return CollectionUtils.isEmpty(watchers) ? Collections.EMPTY_SET : Collections.unmodifiableSet(watchers);
    }

    /**
     * Accessor method
     */
    protected void setWatchers(Set<TrackerUser> watchers) {
        if (CollectionUtils.isNotEmpty(this.watchers)) {
            this.watchers.clear();
        }
        this.watchers = watchers;
    }

    /**
     * Current size of the voters collection.
     *
     * @return number greater or equal than 0
     */
    @Override public Integer getVotes() {
        return getVoters().size();
    }

    /**
     * Current size of the watchers collection.
     *
     * @return number greater or equal than 0
     */
    @Override public Integer getWatcherNumber() {
        return getWatchers().size();
    }

    /**
     * Time of the last operation in this issue.
     *
     * @return any
     */
    public Date getLastModified() {
        return lastModified == null ? null : new Date(lastModified.getTime());
    }

    /**
     * Accessor method.
     *
     * @param lastModified a non null date
     */
    protected void setLastModified(Date lastModified) {
        Assert.notNull(lastModified);
        this.lastModified = lastModified;
    }

    /**
     * All comments made by all users.
     *
     * @return a non enpty unmodifiable collection
     */
    public Set<IssueComment> getComments() {
        return CollectionUtils.isEmpty(comments) ? Collections.EMPTY_SET : comments;
    }

    /**
     * Accessor method.
     *
     * @param comments any
     */
    protected void setComments(Set<IssueComment> comments) {
        if (CollectionUtils.isNotEmpty(this.comments)) {
            this.comments.clear();
        }
        this.comments = comments;
    }

    /**
     * All the attached files of this issue.
     *
     * @return any
     */
    public MultiDocument getAttachments() {
        return attachments;
    }

    /**
     * Accessor method.
     *
     * @param attachments
     */
    protected void setAttachments(MultiDocument attachments) {
        if (this.attachments != null) {
            Set<Document> docs = this.attachments.getDocuments();
            if (CollectionUtils.isNotEmpty(docs)) {
                docs.clear();
            }
        }
        this.attachments = attachments;
    }

    /**
     * The group of links where this entity acts as origin.
     *
     * @return a non null unmodifiable collection
     */
    public Set<Interlink> getLinks() {
        return CollectionUtils.isEmpty(links) ? Collections.EMPTY_SET : links;
    }

    /**
     * Accessor method.
     *
     * @param links any
     */
    protected void setLinks(Set<Interlink> links) {
        if (CollectionUtils.isNotEmpty(links)) {
            links.clear();
        }
        this.links = links;
    }

    /**
     * Modifies the links collection adding a new element (creating the collection altogether if needed).
     *
     * @param link any non null instance
     * @return true if the element did not exist and was added
     */
    protected final boolean addLink(Interlink link) {
        if (links == null) {
            setLinks(new HashSet<Interlink>(1));
        }
        return links.add(link);
    }

    /**
     * Modifies the voters collection adding a new element (creating the collection altogether if needed).
     *
     * @param user any non null instance
     * @return true if the element did not exist and was added
     */
    protected final boolean addVote(TrackerUser user) {
        if (voters == null) {
            voters = new HashSet<TrackerUser>();
        }
        return voters.add(user);
    }

    /**
     * Modifies the watchers collection adding a new element (creating the collection altogether if needed).
     *
     * @param user any non null instance
     * @return true if the element did not exist and was added
     */
    protected final boolean addWatch(TrackerUser user) {
        if (watchers == null) {
            watchers = new HashSet<TrackerUser>();
        }
        return watchers.add(user);
    }

    /**
     * Modifies the comments collection adding a new element (creating the collection altogether if needed).
     *
     * @param comment any non null instance
     * @return true if the element did not exist and was added
     */
    protected final boolean addNewComment(IssueComment comment) {
        if (comments == null) {
            comments = new HashSet<IssueComment>();
        }
        return comments.add(comment);
    }

    /**
     * A simple module representation in the form of "issue: alias"
     *
     * @return a not empty string
     */
    @Override public final String toString() {
        return "issue: " + getAlias();
    }

    /**
     * Delegates to getAlias().
     *
     * @param locale any
     * @return a not empty string
     */
    @Override public String getCaption(Locale locale) {
        return getAlias();
    }

}
